package se.webbzon.boltzmann.menu;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.image.ImageObserver;

import javax.swing.event.MouseInputListener;

import se.webbzon.boltzmann.configuration.Controls;
import se.webbzon.boltzmann.configuration.ControlsListener;
import se.webbzon.boltzmann.game.GameConfiguration;
import se.webbzon.boltzmann.hud.HudCursor;
import se.webbzon.boltzmann.input.Button;
import se.webbzon.boltzmann.input.Input;
import se.webbzon.boltzmann.input.InputListener;
import se.webbzon.oschi01.worldarea.WorldArea;
import se.webbzon.oschi01.worldcamera.WorldCameraPane;
import se.webbzon.oschi01.worldobject.WorldObject;

public class GameMenu extends WorldObject {
	
	// The 2-dimensional depth of the game menu
	private static final int depth = -1;
	
	// The input listener to this game menu
	private final InputListenerImpl inputListener;
	private final ControlsListenerImpl controlListener;
	
	// The parent component to this game menu
	private final Component parent;
	
	// The layout of the game menu
	private final AbstractGameMenuLayout layout;
	
	// The configuration of the game
	private final GameConfiguration config;
	
	// The cursor associated with this game menu
	private final HudCursor cursor;
	
	// The menu page listener of this game menu
	private final MPageListener pageListener;
	
	// A menu page change notifier
	private final Notifier notifier;
	
	// The currently visible menu page
	private MPage page;
	
	// The index of the latest selected item
	private int itemIndex = 0;
	
	// Whatever the position of the cursor should be kept
	// when setting a new page
	private boolean keepCursor; 
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new game menu with a default main page. **/
	public GameMenu(Component parent, AbstractGameMenuLayout layout, GameConfiguration config) {
		this(parent, layout, config, new MainPage(parent,layout));
	}
	
	/** Creates a new game menu with the given layout and main page. **/
	public GameMenu(Component parent, AbstractGameMenuLayout layout, 
					GameConfiguration config, MPage page) {
		super(depth,WorldObject.onCreate + WorldObject.onDestroy + WorldObject.onPostDraw);
		this.parent = parent;
		this.layout = layout;
		this.page = page;
		this.config = config;
		cursor = new HudCursor(layout.getCursorLayout());
		inputListener = new InputListenerImpl();
		controlListener = new ControlsListenerImpl();
		pageListener = new MPageListenerImpl();
		notifier = new Notifier();
		cursor.setVisible(true);
		updateCursor(0);
		page.onEnter(this);
		page.addListener(pageListener);
		page.onRedoLayout();
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Returns the image observer of this game menu. **/
	public ImageObserver getImageObserver() {
		return parent;
	}
	
	/** Returns the configuration of the game. **/
	public GameConfiguration getGameConfiguration() {
		return config;
	}
	
	/** Returns the cursor used in this game menu. **/
	public HudCursor getCursor() {
		return cursor;
	}
	
	/** Returns the currently visible game menu page. **/
	public synchronized MPage getPage() {
		return page;
	}
	
	/** Sets the currently visible game menu page and selects the first item 
	 * in the page if it was not set by an mouse event. **/
	public synchronized void setPage(MPage page) {
		setPage(page,true);
	}
	
	/** Sets the currently visible game menu page. If autoSelect is true then
	 * the first item in the page will be selected if it was not set by an
	 *  mouse event. **/
	public synchronized void setPage(MPage page, boolean autoSelect) {
		itemIndex = 0;
		MPage currentPage = this.page;
		if (currentPage != null) {
			currentPage.onLeave(this);
			currentPage.removeListener(pageListener);
		}
		this.page = page;
		if (page != null) {
			for (int i = 0; i < page.items(); i++)
				page.getItem(i).setFocus(false);
			page.onEnter(this);
			page.addListener(pageListener);
			page.onRedoLayout();
		}
		if (!keepCursor && autoSelect)
			updateCursor(0);
	}
	
	/** Selects the game menu item with the given index in this game menu. **/
	public synchronized void setSelection(int itemIndex) {
		this.itemIndex = itemIndex;
		updateCursor(itemIndex);
	}
	
	/** Selects the given game menu item in this game menu. **/
	public synchronized void setSelection(MItem item) {
		MPage currentPage = this.page;
		for (int i = 0; i < currentPage.items(); i++) {
			if (currentPage.getItem(i) == item) {
				setSelection(i);
				break;
			}
		}
	}
	
	@Override public void onCreate(WorldArea world) {
		world.createInstance(cursor);
		MPage currentPage = page;
		if (currentPage != null)
			currentPage.onEnter(this);
		parent.addMouseListener(inputListener);
		parent.addMouseMotionListener(inputListener);
		Controls.instance().addListener(controlListener);
		Controls.instance().getInput().addListener(inputListener);
	}
	
	@Override public void onDestroy(WorldArea world) {
		world.destroyInstance(cursor);
		MPage currentPage = page;
		if (currentPage != null)
			currentPage.onLeave(this);
		parent.removeMouseListener(inputListener);
		parent.removeMouseMotionListener(inputListener);
		Controls.instance().removeListener(controlListener);
		Controls.instance().getInput().removeListener(inputListener);
	}

	@Override public void onPostDraw(WorldArea world, WorldCameraPane hud) {
		if (page != null) {
			if (notifier.hasChanged())
				page.onRedoLayout();
			page.onDraw(this, hud);
		}
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Converts a point in the window to a point in the page. **/
	private Point getPointOnPage(MouseEvent e) {
		Object s = e.getSource();
		if (s instanceof Component) {
			double w = layout.getPageLayout().getWidth();
			double h = layout.getPageLayout().getHeight();
			Dimension d = ((Component) s).getSize();
			int x = (int) (e.getX() * (w / d.getWidth()));
			int y = (int) (e.getY() * (h / d.getHeight()));
			return new Point(x,y);
		} else
			return e.getPoint();
	}
	
	/** Moves the cursor to the game menu item with the given
	 * index. **/
	private void updateCursor(int itemIndex) {
		MItem item = page.getItem(itemIndex);
		cursor.setPosition(item.getX(),item.getY() + item.getHeight()/2);
		item.setFocus(true);
	}
	
	/*============================================================
	Private Classes
	============================================================*/
	
	private final class InputListenerImpl implements InputListener, MouseInputListener {

		@Override public void mouseClicked(MouseEvent event) {};
	
		@Override public void mouseEntered(MouseEvent event) {};
	
		@Override public void mouseExited(MouseEvent event) {};
	
		@Override public synchronized void mousePressed(MouseEvent event) {
			if (!isLive()) return;
			keepCursor = true;
			Point p = getPointOnPage(event);
			MPage page = GameMenu.this.page;
			for (int i = 0; i < page.items(); i++)
				page.getItem(i).onPress(GameMenu.this, p.x, p.y);
			keepCursor = false;
		}
	
		@Override public synchronized void mouseReleased(MouseEvent event) {
			if (!isLive()) return;
			keepCursor = true;
			Point p = getPointOnPage(event);
			MPage page = GameMenu.this.page;
			for (int i = 0; i < page.items(); i++)
				page.getItem(i).onRelease(GameMenu.this, p.x, p.y);
			keepCursor = false;
		}
	
		@Override public void primaryAxisChanged(Input input, double direction, double value) {};
	
		@Override public void secondaryAxisChanged(Input input, double direction, double value) {};
	
		@Override public synchronized void buttonPressed(Input input, Button button) { 
			if (!isLive()) return;
			MPage page = GameMenu.this.page;
			MItem item;
			switch (button) {
			case UP:
				// Previous menu item
				if (itemIndex == 0)
					itemIndex = page.items() - 1;
				else
					itemIndex--;
				updateCursor(itemIndex);
				break;
			case DOWN:
				// Next menu item
				if (itemIndex >= page.items() - 1)
					itemIndex = 0;
				else
					itemIndex++;
				updateCursor(itemIndex);
				break;
			case ACTION:
				// Press the menu item
				item = page.getItem(itemIndex);
				if (item.isWithin(cursor.getX(), cursor.getY()))
					item.onPress(GameMenu.this, item.getX(), item.getY());
				return;
			case LEFT:
				// Drag the menu item to the left
				item = page.getItem(itemIndex);
				if (item.isDraggable()) {
					item.onPress(GameMenu.this, item.getX(), item.getY());
					item.onDrag(GameMenu.this, item.getDragX() - item.getWidth()/10, item.getDragY());
					item.onRelease(GameMenu.this, item.getX(), item.getY());
				}
				return;
			case RIGHT:
				// Drag the menu item to the right
				item = page.getItem(itemIndex);
				if (item.isDraggable()) {
					item.onPress(GameMenu.this, item.getX(), item.getY());
					item.onDrag(GameMenu.this, item.getDragX() + item.getWidth()/10, item.getDragY());
					item.onRelease(GameMenu.this, item.getX(), item.getY());
				}
				return;
			case JUMP:
				if (page.hasParent())
					setPage(page.getParent());
				return;
			default:
				return;
			}
	
			for (int i = 0; i < page.items(); i++) {
				MItem it = page.getItem(i);
				if (i == itemIndex)
					it.setFocus(true);
				else
					it.setFocus(false);
			}	
		}
	
		@Override public synchronized void buttonReleased(Input input, Button button) {
			if (!isLive()) return;
			if (button == Button.ACTION) {
				// Release the menu item
				MItem item = page.getItem(itemIndex);
				MPage page = GameMenu.this.page;
				for (int i = 0; i < page.items(); i++)
					page.getItem(i).onRelease(GameMenu.this, item.getX(), item.getY());
			}
		}
	
		@Override public void bindingAccepted(Input input, Button button) {};
	
		@Override public synchronized void mouseDragged(MouseEvent event) {
			if (!isLive()) return;
			keepCursor = true;
			MPage page = GameMenu.this.page;
			Point p = getPointOnPage(event);
			int x = p.x;
			int y = p.y;
			for (int i = 0; i < page.items(); i++) {
				MItem item = page.getItem(i);
				if (item.isDraggable())
					item.onDrag(GameMenu.this, x, y);
			}
			cursor.setPosition(x, y);
			keepCursor = false;
		}
	
		@Override public synchronized void mouseMoved(MouseEvent event) {
			if (!isLive()) return;
			keepCursor = true;
			itemIndex = 0;
			Point p = getPointOnPage(event);
			int x = p.x;
			int y = p.y;
			cursor.setPosition(x, y);
			MPage page = GameMenu.this.page;
			for (int i = 0; i < page.items(); i++) {
				MItem item = page.getItem(i);
				if (item.isWithin(x, y))
					item.setFocus(true);
				else
					item.setFocus(false);
			}
			keepCursor = false;
		}
	
	}
	
	private final class ControlsListenerImpl implements ControlsListener {

		@Override public void onAddInput(Input input) {}

		@Override public void onRemoveInput(Input input) {}

		@Override public void onChangeInput(Input oldInput, Input newInput) {
			oldInput.removeListener(inputListener);
			newInput.addListener(inputListener);
		}
		
	}
	
	private final class MPageListenerImpl implements MPageListener {

		@Override public void onItemAdded(MPage page, MItem item) {
			notifier.change();
		}

		@Override public void onItemRemoved(MPage page, MItem item) {
			notifier.change();
		}

		@Override public void onClear(MPage page) {
			notifier.change();
		}
		
	}
	
	private final class Notifier {
		
		private boolean changed = false;
		
		public synchronized void change() {
			changed = true;
		}
		
		public synchronized boolean hasChanged() {
			if (changed) {
				changed = false;
				return true;
			} else
				return false;
		}
		
	}
	
}
